跳到主要内容

创建型模式-单例模式

单例模式是什么?

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于 创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象

1、单例类 只能有一个 实例。 2、单例类必须 自己创建自己 的唯一实例。 3、单例类必须给 所有其他对象 提供这一实例。

懒汉式单例

  • 适用于单线程环境(不推荐)
  • 适用于多线程环境,但效率不高(不推荐)
  • 双重检验锁
  • 静态内部类方式(推荐)

饿汉式单例

  • 饿汉式(推荐)
  • 枚举方式(推荐)

饿汉式(常用)

是否多线程安全:是

描述:这种方式比较常用,但容易产生垃圾对象。

  • 优点:没有加锁,执行效率会提高。
  • 缺点:类加载时就初始化,浪费内存。

它基于 classloader 机制避免了多线程的同步问题,但是 instance 在类装载时就实例化 如果没有用到这个实例,那这个内存就浪费了

SingleObject.java


public class SingleObject {

//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();

//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}

//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}

public void showMessage(){
System.out.println("Hello World!");
}
}

枚举方式(推荐)

创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且还能防止反序列化导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。

public class Temp {
public static void main(String[] args) {
Temp single = Temp.getInstance(); // 获取单例
}

private Temp() {}

public static Temp getInstance() {
return Singleton.INSTANCE.getInstance();
}

// 枚举是线程安全的,并且只会装载一次
private enum Singleton {
INSTANCE;
private final Temp instance;

Singleton() {
instance = new Temp();
}

private Temp getInstance() {
return instance;
}
}
}

懒汉式(线程不安全)

public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

懒汉式(线程安全)

使用同步锁 synchronized 防止多线程同时进入造成 instance 被多次实例化。

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,因为实际情况中 99% 情况下不需要同步。(只有第一次创建时需要加锁)

  • 优点:第一次调用才初始化,避免内存浪费。
  • 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

双重检验锁

为了在多线程环境下,不影响程序的性能,不让线程每次调用 getInstance() 方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在。

public static Singleton getInstance() {
// 先判断实例是否存在,若不存在再对类对象进行加锁处理
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton1();//error
}
}
}
return instance;
}

下面是上述代码的运行顺序:

  1. 检测实例是否已经初始化创建,如果是则立即返回
  2. 否则获得锁
  3. 再次检测实例是否已经初始化创建成功,如果还没有则创建实例

执行双重检测是因为,如果多个线程通过了第一次检测,并且其中一个首先通过了第二次检测并实例化了对象,剩余的线程不会再重复实例化对象。这样,除了初始化的时候会加锁,后续的调用都是直接返回,解决了多余的性能消耗。

双重检验锁的隐患

但是!!真的这样写是不对的,如下所示,在 IDEA 中抛出了警告

看似天衣无缝,但是这种实现是有隐患的,这个隐患来自于上述代码中注释了 error 的一行,这行代码大致有以下三个步骤:

  1. 在堆中开辟对象所需空间,分配地址
  2. 根据类加载的初始化顺序进行初始化
  3. 将内存地址返回给栈中的引用变量

由于 Java 内存模型允许 “无序写入”,有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了

  1. 在堆中开辟对象所需空间,分配地址
  2. 将内存地址返回给栈中的引用变量(此时变量已不在为 null,但是变量却并没有初始化完成)
  3. 根据类加载的初始化顺序进行初始化

现在考虑重排序后,两个线程出现了如下调用:

此时 T7 时刻 Thread B 对 instance 的访问,访问到的是一个还未完成初始化的对象。所以在使用 instance 时可能会出错。

这时就需要使用到 volatile 来解决内存重排的问题:

public class Singleton {

private static volatile Singleton instance = null;

private Singleton (){}

public static Singleton getInstance() {
// 先判断实例是否存在,若不存在再对类对象进行加锁处理
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

静态内部类方式

加载一个类时,其静态内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

由于在调用 StaticSingleton.getInstance() 的时候,才会对单例进行初始化,由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。

  • 优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
  • 劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。
public class StaticSingleton {
private StaticSingleton() {}

public static StaticSingleton getInstance() {
return StaticSingletonHolder.instance;
}

/**
* 一个私有的静态内部类,用于初始化一个静态final实例
*/
private static class StaticSingletonHolder {
private static final StaticSingleton instance = new StaticSingleton();
}

public void methodA() {/* ... */}

public void methodB() {/* ... */}

public static void main(String[] args) {
StaticSingleton.getInstance().methodA();
StaticSingleton.getInstance().methodB();
}
}

Reference

“双重检查锁定被破坏” 的原因 单例模式(双重检测锁式单例不安全的原因与解决) 代码参考